package br.ufc.ck0017.visitor.typechecking;

import br.ufc.ck0017.semantic.model.SemanticClass;
import br.ufc.ck0017.semantic.model.SemanticMethod;
import br.ufc.ck0017.semantic.model.SemanticProgram;
import br.ufc.ck0017.syntaxtree.*;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by fzschornack on 21/05/15.
 */
public class TypeCheckVisitor extends TypeDepthFirstVisitor {

    private SemanticClass currentClass;
    private SemanticMethod currentMethod;

    private final SemanticProgram program;
    private final List<String> errors;

    public TypeCheckVisitor(SemanticProgram program) {
        this.program = program;
        errors = new ArrayList<>();
    }

    public List<String> getErrorMessages() {
        return new ArrayList<>(errors);
    }

    // MainClass mainClass;
    // ClassDeclList classDeclList;
    public Type visit(Program n){
        n.mainClass.accept(this);

        for(int i = 0; i < n.classDeclList.size(); i++ )
            n.classDeclList.elementAt(i).accept(this);

        return null;
    }

    // Identifier i1,i2;
    // IRStatement s;
    public Type visit(MainClass n){
        currentClass = program.getClass(n.i1.s); //inside class scope

        n.s.accept(this);

        currentClass = null; //end class scope

        return null;
    }

    // Identifier i;
    // VarDeclList vl;
    // MethodDeclList ml;
    public Type visit(ClassDeclSimple n) {
        currentClass = program.getClass(n.i.s); //inside class scope

        for (int i = 0; i < n.vl.size(); i++) {
            n.vl.elementAt(i).accept(this);
        }

        for (int i = 0; i < n.ml.size(); i++) {
            n.ml.elementAt(i).accept(this);
        }

        currentClass = null; //end class scope

        return null;
    }

    // Identifier i;
    // Identifier j;
    // VarDeclList vl;
    // MethodDeclList ml;
    public Type visit(ClassDeclExtends n) {
        currentClass = program.getClass(n.i.s); //inside class scope

        if (program.getClass(n.j.s) == null) {
            errors.add(ErrorMessage.NO_PARENT_CLASS);
        }

        for (int i = 0; i < n.vl.size(); i++) {
            n.vl.elementAt(i).accept(this);
        }

        for (int i = 0; i < n.ml.size(); i++) {
            n.ml.elementAt(i).accept(this);
        }

        currentClass = null; //end class scope

        return null;
    }

    // Type t;
    // Identifier i;
    public Type visit(VarDecl n){
        // nothing to do
        return null;
    }

    // Type t;
    // Identifier i;
    // FormalList fl; (method parameters)
    // VarDeclList vl;
    // StatementList sl;
    // Exp e;
    public Type visit(MethodDecl n) {
        currentMethod = currentClass.getMethod(n.i.toString()); //inside method scope

        n.t.accept(this);
        n.i.accept(this);

        for (int i = 0; i < n.fl.size(); i++) {
            n.fl.elementAt(i).accept(this);
        }

        for (int i = 0; i < n.vl.size(); i++) {
            n.vl.elementAt(i).accept(this);
        }

        for (int i = 0; i < n.sl.size(); i++) {
            n.sl.elementAt(i).accept(this);
        }

        Type returnType = n.e.accept(this);
        if (!returnType.getClass().getName().equals(currentMethod.getType().getClass().getName())) {
            errors.add(ErrorMessage.WRONG_METHODS_RETURN_TYPE);
        }

        currentMethod = null; //end method scope

        return null;
    }

    // Type t;
    // Identifier i;
    public Type visit(Formal n){
        // nothing to do
        return null;
    }

    // Exp e;
    // IRStatement s1,s2;
    public Type visit(If n) {
        if (!(n.e.accept(this) instanceof BooleanType)) {
            errors.add(ErrorMessage.MUST_BE_BOOLEAN);
        }
        n.s1.accept(this);
        n.s2.accept(this);

        return null;
    }

    // Exp e;
    // IRStatement s;
    public Type visit(While n) {
        if (!(n.e.accept(this) instanceof BooleanType)) {
            errors.add(ErrorMessage.MUST_BE_BOOLEAN);
        }
        n.s.accept(this);
        return null;
    }

    // Identifier i;
    // Exp e;
    public Type visit(Assign n) {

        Type variableType = lookupType(n.i.s);
        Type expType = n.e.accept(this);

        // if variableType is some Class
        if (variableType instanceof IdentifierType) {
            SemanticClass sc = program.getClass( ((IdentifierType) variableType).s ); //check if class is declared
            if (sc == null
                    || (!sc.getName().equals( ((IdentifierType) expType).s ) && (sc.getParent() != null && !sc.getParent().getName().equals( ((IdentifierType) expType).s )))) {
                errors.add(ErrorMessage.WRONG_RIGHT_SIDE_TYPE);
            }
        } else {
            if (variableType == null || expType == null
                    || !variableType.getClass().getName().equals(expType.getClass().getName())) {
                errors.add(ErrorMessage.WRONG_RIGHT_SIDE_TYPE);
            }
        }

        return null;
    }

    // Identifier i;
    // Exp e1,e2;
    public Type visit(ArrayAssign n) {
        Type arrayType = lookupType(n.i.s);
        Type indexerType = n.e1.accept(this);
        Type valueType = n.e2.accept(this);

        if (arrayType == null) {
            errors.add(ErrorMessage.NO_SUCH_VAR);
            return null;
        }
        if (!(arrayType instanceof IntArrayType)) {
            errors.add(ErrorMessage.MUST_BE_ARRAY);
        }
        if (!(indexerType instanceof IntegerType)) {
            errors.add(ErrorMessage.MUST_BE_INT);
        }
        if (!(valueType instanceof IntegerType)) {
            errors.add(ErrorMessage.WRONG_RIGHT_SIDE_TYPE);
        }

        return null;
    }

    // Exp e1, e2;
    public Type visit(And n) {
        if (!(n.e1.accept(this) instanceof BooleanType)) {
            errors.add(ErrorMessage.MUST_BE_BOOLEAN);
        }
        if (!(n.e2.accept(this) instanceof BooleanType)) {
            errors.add(ErrorMessage.MUST_BE_BOOLEAN);
        }
        return new BooleanType();
    }

    // Exp e1, e2;
    public Type visit(LessThan n) {
        if (!(n.e1.accept(this) instanceof IntegerType)) {
            errors.add(ErrorMessage.MUST_BE_INT);
        }
        if (!(n.e2.accept(this) instanceof IntegerType)) {
            errors.add(ErrorMessage.MUST_BE_INT);
        }
        return new BooleanType();
    }

    // Exp e1, e2;
    public Type visit(Plus n) {
        if (!(n.e1.accept(this) instanceof IntegerType)) {
            errors.add(ErrorMessage.MUST_BE_INT);
        }
        if (!(n.e2.accept(this) instanceof IntegerType)) {
            errors.add(ErrorMessage.MUST_BE_INT);
        }
        return new IntegerType();
    }

    // Exp e1, e2;
    public Type visit(Minus n) {
        if (!(n.e1.accept(this) instanceof IntegerType)) {
            errors.add(ErrorMessage.MUST_BE_INT);
        }
        if (!(n.e2.accept(this) instanceof IntegerType)) {
            errors.add(ErrorMessage.MUST_BE_INT);
        }
        return new IntegerType();
    }

    // Exp e1, e2;
    public Type visit(Times n) {
        if (!(n.e1.accept(this) instanceof IntegerType)) {
            errors.add(ErrorMessage.MUST_BE_INT);
        }
        if (!(n.e2.accept(this) instanceof IntegerType)) {
            errors.add(ErrorMessage.MUST_BE_INT);
        }
        return new IntegerType();
    }

    // Exp e1, e2;
    public Type visit(ArrayLookup n) {
        Type arrayType = n.e1.accept(this);
        Type indexerType = n.e2.accept(this);
        if (!(arrayType instanceof IntArrayType)) {
            errors.add(ErrorMessage.MUST_BE_ARRAY);
        }
        if (!(indexerType instanceof IntegerType)) {
            errors.add(ErrorMessage.MUST_BE_INT);
        }
        return new IntegerType();
    }

    // Exp e;
    public Type visit(ArrayLength n) {
        Type expType = n.e.accept(this);
        if (!(expType instanceof IntArrayType)) {
            errors.add(ErrorMessage.MUST_BE_ARRAY);
        }
        return new IntegerType();
    }

    // Exp e;
    // Identifier i;
    // ExpList el;
    public Type visit(Call n) {
        Type instanceType = n.e.accept(this);
        if (instanceType == null || !(instanceType instanceof IdentifierType)) {
            errors.add(ErrorMessage.MUST_BE_OBJECT);
        }

        // verify if the method exists
        SemanticClass instance = program.getClass( ((IdentifierType) instanceType).s );
        SemanticMethod method = instance.getMethod(n.i.s);
        if (method == null) {
            errors.add(ErrorMessage.NO_SUCH_METHOD);
            return null;
        }

        // verify if the right number of parameters is passed to the method
        int paramCount = n.el.size();
        int declaredParamCount = method.getParametersSize();
        if (paramCount != declaredParamCount) {
            errors.add(ErrorMessage.NO_SUCH_METHOD);
            return null;
        }

        // check the parameters types
        for (int i = 0; i < n.el.size(); i++) {
            Type paramType = n.el.elementAt(i).accept(this);
            Type declaredParamType = method.getArgsTypes().get(i);

            if (paramType instanceof IdentifierType) {
                SemanticClass sc = program.getClass(((IdentifierType) paramType).s);
                if (sc == null
                        || (!sc.getName().equals( ((IdentifierType) declaredParamType).s ) && (sc.getParent() != null && !sc.getParent().getName().equals( ((IdentifierType) declaredParamType).s )))) {
                    errors.add(ErrorMessage.WRONG_RIGHT_SIDE_TYPE);
                }
            } else {
                if (!paramType.getClass().getName().equals(declaredParamType.getClass().getName())) {
                    errors.add(ErrorMessage.WRONG_RIGHT_SIDE_TYPE);
                }
            }
        }

        return method.getType();
    }

    public Type visit(IntegerLiteral n) {
        return new IntegerType();
    }

    public Type visit(True n) {
        return new BooleanType();
    }

    public Type visit(False n) {
        return new BooleanType();
    }

    // String s;
    public Type visit(IdentifierExp n) {
        Type t = lookupType(n.s);
        if (t == null) {
            errors.add(ErrorMessage.NO_SUCH_OBJECT);
        }
        return t;
    }

    public Type visit(This n) {
        // Keyword 'this' refers to the current class.
        if (currentClass == null) {
            errors.add(ErrorMessage.OUT_OF_INSTANCE_SCOPE);
        }
        return new IdentifierType(currentClass.getName());
    }

    // Exp e;
    public Type visit(NewArray n) {
        Type t = n.e.accept(this);
        if (!(t instanceof IntegerType)) {
            errors.add(ErrorMessage.WRONG_INDEX_TYPE);
        }
        return new IntArrayType();
    }

    // Identifier i;
    public Type visit(NewObject n) {
        SemanticClass sc = program.getClass(n.i.s);
        if (sc == null) {
            errors.add(ErrorMessage.NO_SUCH_CLASS);
        }
        return n.i.accept(this);
    }

    // Exp e;
    public Type visit(Not n) {
        n.e.accept(this);
        return new BooleanType();
    }

    // String s;
    public Type visit(Identifier n) {
        return lookupType(n.s);
    }

    protected Type lookupType(String id) {
        if (currentMethod != null && currentMethod.getArg(id) != null) {
            return currentMethod.getArg(id).getType();
        }
        if (currentMethod != null && currentMethod.getVar(id) != null) {
            return currentMethod.getVar(id).getType();
        }
        if (currentClass != null && currentClass.getField(id) != null) {
            return currentClass.getField(id).getType();
        }
        if (program.getClass(id) != null) {
            // it is a class name
            return new IdentifierType(id);
        }

        // it is declared in parent class
        SemanticClass parentClass = currentClass.getParent();
        if (parentClass != null && program.getClass(parentClass.getName()).getField(id) != null) {
            return program.getClass(parentClass.getName()).getField(id).getType();
        }

        return null;
    }

}
